#ifndef SIMPLE_SUPPORT_ALGORITHM_RANGE_WRAPPERS_HPP
#define SIMPLE_SUPPORT_ALGORITHM_RANGE_WRAPPERS_HPP

// wrapping these
#include <algorithm>
#include <string_view>

#include "traits.hpp" // support::is_range_v
#include "../function_utils.hpp" // support::invoke
#include "adl_helper.hpp" // adl_helper::adl_begin adl_helper::adl_end

namespace simple::support
{

	//TODO: all noexcepts bellow should account for return value construction

	template <typename Range,
		std::enable_if_t<is_range_v<Range>>* = nullptr>
	constexpr auto min_element(Range& range)
	noexcept(noexcept(std::min_element(adl_helper::adl_begin(range), adl_helper::adl_end(range))))
	{
		return std::min_element(adl_helper::adl_begin(range), adl_helper::adl_end(range));
	}

	template <typename Range,
		std::enable_if_t<is_range_v<Range>>* = nullptr>
	constexpr auto min_element(Range&& range)
	noexcept(noexcept(std::min_element(adl_helper::adl_begin(range), adl_helper::adl_end(range))))
	{
		return *std::min_element(adl_helper::adl_begin(range), adl_helper::adl_end(range));
	}

	template <typename Range,
		std::enable_if_t<is_range_v<Range>>* = nullptr>
	constexpr auto max_element(Range& range)
	noexcept(noexcept(std::max_element(adl_helper::adl_begin(range), adl_helper::adl_end(range))))
	{
		return std::max_element(adl_helper::adl_begin(range), adl_helper::adl_end(range));
	}

	template <typename Range,
		std::enable_if_t<is_range_v<Range>>* = nullptr>
	constexpr auto max_element(Range&& range)
	noexcept(noexcept(std::max_element(adl_helper::adl_begin(range), adl_helper::adl_end(range))))
	{
		return *std::max_element(adl_helper::adl_begin(range), adl_helper::adl_end(range));
	}

	template <typename Range, typename Compare,
		std::enable_if_t<is_range_v<Range>>* = nullptr>
	constexpr auto min_element(Range& range, Compare comp)
	noexcept(noexcept(std::min_element(adl_helper::adl_begin(range), adl_helper::adl_end(range), std::move(comp))))
	{
		return std::min_element(adl_helper::adl_begin(range), adl_helper::adl_end(range), std::move(comp));
	}

	template <typename Range, typename Compare,
		std::enable_if_t<is_range_v<Range>>* = nullptr>
	constexpr auto min_element(Range&& range, Compare comp)
	noexcept(noexcept(std::min_element(adl_helper::adl_begin(range), adl_helper::adl_end(range), std::move(comp))))
	{
		return *std::min_element(adl_helper::adl_begin(range), adl_helper::adl_end(range), std::move(comp));
	}

	template <typename Range, typename Compare,
		std::enable_if_t<is_range_v<Range>>* = nullptr>
	constexpr auto max_element(Range& range, Compare comp)
	noexcept(noexcept(std::max_element(adl_helper::adl_begin(range), adl_helper::adl_end(range), std::move(comp))))
	{
		return std::max_element(adl_helper::adl_begin(range), adl_helper::adl_end(range), std::move(comp));
	}

	template <typename Range, typename Compare,
		std::enable_if_t<is_range_v<Range>>* = nullptr>
	constexpr auto max_element(Range&& range, Compare comp)
	noexcept(noexcept(std::max_element(adl_helper::adl_begin(range), adl_helper::adl_end(range), std::move(comp))))
	{
		return *std::max_element(adl_helper::adl_begin(range), adl_helper::adl_end(range), std::move(comp));
	}

	template <typename Range, typename UnaryPredicate,
		std::enable_if_t<is_range_v<Range>>* = nullptr>
	constexpr auto all_of(const Range& range, UnaryPredicate p)
	noexcept(noexcept(std::all_of(adl_helper::adl_begin(range), adl_helper::adl_end(range), std::move(p))))
	{
		return std::all_of(adl_helper::adl_begin(range), adl_helper::adl_end(range), std::move(p));
	}
	template <typename ExecutionPolicy, typename Range, typename UnaryPredicate,
		// TODO: use std::is_execution_policy_v when it stops depending on external tbb
		std::enable_if_t<not is_range_v<ExecutionPolicy>>* = nullptr,
		std::enable_if_t<is_range_v<Range>>* = nullptr>
	constexpr auto all_of(ExecutionPolicy&& policy, const Range& range, UnaryPredicate p)
	noexcept(noexcept(std::all_of(std::forward<ExecutionPolicy>(policy),
		adl_helper::adl_begin(range), adl_helper::adl_end(range), std::move(p))))
	{
		return std::all_of(std::forward<ExecutionPolicy>(policy),
			adl_helper::adl_begin(range), adl_helper::adl_end(range), std::move(p));
	}

	template <typename Range, typename UnaryPredicate,
		std::enable_if_t<is_range_v<Range>>* = nullptr>
	constexpr auto any_of(const Range& range, UnaryPredicate p)
	noexcept(noexcept(std::any_of(adl_helper::adl_begin(range), adl_helper::adl_end(range), std::move(p))))
	{
		return std::any_of(adl_helper::adl_begin(range), adl_helper::adl_end(range), std::move(p));
	}
	template <typename ExecutionPolicy, typename Range, typename UnaryPredicate,
		// TODO: use std::is_execution_policy_v when it stops depending on external tbb
		std::enable_if_t<not is_range_v<ExecutionPolicy>>* = nullptr,
		std::enable_if_t<is_range_v<Range>>* = nullptr>
	constexpr auto any_of(ExecutionPolicy&& policy, const Range& range, UnaryPredicate p)
	noexcept(noexcept(std::any_of(std::forward<ExecutionPolicy>(policy),
		adl_helper::adl_begin(range), adl_helper::adl_end(range), std::move(p))))
	{
		return std::any_of(std::forward<ExecutionPolicy>(policy),
			adl_helper::adl_begin(range), adl_helper::adl_end(range), std::move(p));
	}

	template <typename Range, typename UnaryPredicate,
		std::enable_if_t<is_range_v<Range>>* = nullptr>
	constexpr auto none_of(const Range& range, UnaryPredicate p)
	noexcept(noexcept(std::none_of(adl_helper::adl_begin(range), adl_helper::adl_end(range), std::move(p))))
	{
		return std::none_of(adl_helper::adl_begin(range), adl_helper::adl_end(range), std::move(p));
	}
	template <typename ExecutionPolicy, typename Range, typename UnaryPredicate,
		// TODO: use std::is_execution_policy_v when it stops depending on external tbb
		std::enable_if_t<not is_range_v<ExecutionPolicy>>* = nullptr,
		std::enable_if_t<is_range_v<Range>>* = nullptr>
	constexpr auto none_of(ExecutionPolicy&& policy, const Range& range, UnaryPredicate p)
	noexcept(noexcept(std::none_of(std::forward<ExecutionPolicy>(policy),
		adl_helper::adl_begin(range), adl_helper::adl_end(range), std::move(p))))
	{
		return std::none_of(std::forward<ExecutionPolicy>(policy),
			adl_helper::adl_begin(range), adl_helper::adl_end(range), std::move(p));
	}

	template <typename ExecutionPolicy, typename Range, typename OutIt, typename UnaryPredicate,
		// TODO: use std::is_execution_policy_v when it stops depending on external tbb
		std::enable_if_t<not is_range_v<ExecutionPolicy>>* = nullptr,
		std::enable_if_t<is_range_v<Range>>* = nullptr>
	constexpr auto copy_if(ExecutionPolicy&& policy, const Range& range, UnaryPredicate p, OutIt out)
	noexcept(noexcept(
		std::copy_if(std::forward<ExecutionPolicy>(policy),
			adl_helper::adl_begin(range), adl_helper::adl_end(range), out, std::move(p))
	))
	{
		return std::copy_if(std::forward<ExecutionPolicy>(policy),
			adl_helper::adl_begin(range), adl_helper::adl_end(range), out, std::move(p));
	}

	template <typename Range, typename OutIt, typename UnaryPredicate,
		std::enable_if_t<is_range_v<Range>>* = nullptr>
	constexpr auto copy_if(const Range& range, UnaryPredicate p, OutIt out)
	noexcept(noexcept(std::copy_if(adl_helper::adl_begin(range), adl_helper::adl_end(range), out, std::move(p))))
	{
		return std::copy_if(adl_helper::adl_begin(range), adl_helper::adl_end(range), out, std::move(p));
	}

	template <typename ExecutionPolicy, typename Range, typename OutIt,
		// TODO: use std::is_execution_policy_v when it stops depending on external tbb
		std::enable_if_t<not is_range_v<ExecutionPolicy>>* = nullptr,
		std::enable_if_t<is_range_v<Range>>* = nullptr>
	constexpr auto copy(ExecutionPolicy&& policy, const Range& range, OutIt out)
	noexcept(noexcept(
		std::copy(std::forward<ExecutionPolicy>(policy),
			adl_helper::adl_begin(range), adl_helper::adl_end(range), out)
	))
	{
		return std::copy(std::forward<ExecutionPolicy>(policy),
			adl_helper::adl_begin(range), adl_helper::adl_end(range), out);
	}

	template <typename Range, typename OutIt,
		std::enable_if_t<is_range_v<Range>>* = nullptr>
	constexpr auto copy(const Range& range, OutIt out)
	noexcept(noexcept(std::copy(adl_helper::adl_begin(range), adl_helper::adl_end(range), out)))
	{
		return std::copy(adl_helper::adl_begin(range), adl_helper::adl_end(range), out);
	}

	template <typename ExecutionPolicy, typename Range, typename It, typename OutIt, typename BinaryOperation,
		// TODO: use std::is_execution_policy_v when it stops depending on external tbb
		std::enable_if_t<not is_range_v<ExecutionPolicy>>* = nullptr,
		std::enable_if_t<is_range_v<Range>>* = nullptr>
	constexpr auto transform(ExecutionPolicy&& policy, const Range& range, It it, BinaryOperation op, OutIt out)
	noexcept(noexcept(
		std::transform(std::forward<ExecutionPolicy>(policy),
			adl_helper::adl_begin(range), adl_helper::adl_end(range),
			it, out, std::move(op))
	))
	{
		return std::transform(std::forward<ExecutionPolicy>(policy),
			adl_helper::adl_begin(range), adl_helper::adl_end(range),
			it, out, std::move(op));
	}

	template <typename Range, typename It, typename OutIt, typename BinaryOperation,
		std::enable_if_t<is_range_v<Range>>* = nullptr>
	constexpr auto transform(const Range& range, It it, BinaryOperation op, OutIt out)
	noexcept(noexcept(
		std::transform(adl_helper::adl_begin(range), adl_helper::adl_end(range),
			it, out, std::move(op))
	))
	{
		return std::transform(adl_helper::adl_begin(range), adl_helper::adl_end(range),
			it, out, std::move(op));
	}

	template <typename ExecutionPolicy, typename Range, typename OutIt, typename UnaryOperation,
		// TODO: use std::is_execution_policy_v when it stops depending on external tbb
		std::enable_if_t<not is_range_v<ExecutionPolicy>>* = nullptr,
		std::enable_if_t<is_range_v<Range>>* = nullptr>
	constexpr auto transform(ExecutionPolicy&& policy, const Range& range, UnaryOperation op, OutIt out)
	noexcept(noexcept(
		std::transform(std::forward<ExecutionPolicy>(policy),
			adl_helper::adl_begin(range), adl_helper::adl_end(range),
			out, std::move(op))
	))
	{
		return std::transform(std::forward<ExecutionPolicy>(policy),
			adl_helper::adl_begin(range), adl_helper::adl_end(range),
			out, std::move(op));
	}

	template <typename Range, typename OutIt, typename UnaryOperation,
		std::enable_if_t<is_range_v<Range>>* = nullptr>
	constexpr auto transform(const Range& range, UnaryOperation op, OutIt out)
	noexcept(noexcept(
		std::transform(adl_helper::adl_begin(range), adl_helper::adl_end(range),
			out, std::move(op))
	))
	{
		return std::transform(adl_helper::adl_begin(range), adl_helper::adl_end(range),
			out, std::move(op));
	}

	// TODO: have own find now since libstdc++ is too clever, but not clever enough to SFINAE it's cleverness, so will have to implement type constraints for standard iterator and dispatch on that

	template <typename ExecutionPolicy, typename Range, typename UnaryPredicate,
		// TODO: use std::is_execution_policy_v when it stops depending on external tbb
		std::enable_if_t<not is_range_v<ExecutionPolicy>>* = nullptr,
		std::enable_if_t<is_range_v<Range>>* = nullptr>
	constexpr auto find_if(ExecutionPolicy&& policy, const Range& range, UnaryPredicate p)
	noexcept(noexcept(
		std::find_if(std::forward<ExecutionPolicy>(policy),
			adl_helper::adl_begin(range), adl_helper::adl_end(range), std::move(p))
	))
	{
		return std::find_if(std::forward<ExecutionPolicy>(policy),
			adl_helper::adl_begin(range), adl_helper::adl_end(range), std::move(p));
	}

	template <typename Range, typename UnaryPredicate,
		std::enable_if_t<is_range_v<Range>>* = nullptr>
	constexpr auto find_if(const Range& range, UnaryPredicate p)
	noexcept(noexcept(
		std::find_if(adl_helper::adl_begin(range), adl_helper::adl_end(range), std::move(p))
	))
	{
		return std::find_if(adl_helper::adl_begin(range), adl_helper::adl_end(range), std::move(p));
	}

	template <typename ExecutionPolicy, typename Range, typename Value,
		// TODO: use std::is_execution_policy_v when it stops depending on external tbb
		std::enable_if_t<not is_range_v<ExecutionPolicy>>* = nullptr,
		std::enable_if_t<is_range_v<Range>>* = nullptr>
	constexpr auto find(ExecutionPolicy&& policy, const Range& range, const Value& value)
	noexcept(noexcept(
		std::find(std::forward<ExecutionPolicy>(policy),
			adl_helper::adl_begin(range), adl_helper::adl_end(range), value)
	))
	{
		return std::find(std::forward<ExecutionPolicy>(policy),
			adl_helper::adl_begin(range), adl_helper::adl_end(range), value);
	}

	template <typename Range, typename Value,
		std::enable_if_t<is_range_v<Range>>* = nullptr>
	constexpr auto find(const Range& range, const Value& value)
	noexcept(noexcept(
		std::find(adl_helper::adl_begin(range), adl_helper::adl_end(range), value)
	))
	{
		return std::find(adl_helper::adl_begin(range), adl_helper::adl_end(range), value);
	}

	template <typename ExecutionPolicy, typename Range, typename Value, typename BinaryPredicate,
		// TODO: use std::is_execution_policy_v when it stops depending on external tbb
		std::enable_if_t<not is_range_v<ExecutionPolicy>>* = nullptr,
		std::enable_if_t<is_range_v<Range>>* = nullptr>
	constexpr auto find(ExecutionPolicy&& policy, const Range& range, const Value& v, BinaryPredicate&& p)
	noexcept(noexcept(
		std::find(std::forward<ExecutionPolicy>(policy),
			adl_helper::adl_begin(range), adl_helper::adl_end(range), v),
		support::invoke(std::forward<BinaryPredicate>(p), *adl_helper::adl_begin(range), v)
	))
	{
		return std::find_if(std::forward<ExecutionPolicy>(policy),
			adl_helper::adl_begin(range), adl_helper::adl_end(range),
			[&](auto&& x) { return support::invoke(std::forward<BinaryPredicate>(p), x, v); }
		);
	}

	template <typename Range, typename Value, typename BinaryPredicate,
		std::enable_if_t<is_range_v<Range>>* = nullptr>
	constexpr auto find(const Range& range, const Value& v, BinaryPredicate&& p)
	noexcept(noexcept(
		std::find(adl_helper::adl_begin(range), adl_helper::adl_end(range), v),
		support::invoke(std::forward<BinaryPredicate>(p), *adl_helper::adl_begin(range), v)
	))
	{
		return std::find_if(adl_helper::adl_begin(range), adl_helper::adl_end(range),
			[&](auto&& x) { return support::invoke(std::forward<BinaryPredicate>(p), x, v); }
		);
	}

} // namespace simple::support

#endif /* end of include guard */
